Skip to content

Automated Test: blocked-email-validation-post #310

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features
    • Introduced email blocking system to prevent specific email addresses from creating accounts.
    • Enhanced email validation with improved error messages and domain restriction enforcement.
    • Implemented tracking of rejected emails during account creation to prevent repeated failed attempts.

… many times each email address is blocked, and last time it was blocked. Move email validation out of User model and into EmailValidator. Signup form remembers which email addresses have failed and shows validation error on email field.
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This pull request refactors email validation logic by introducing a new BlockedEmail model and EmailValidator class to centralize domain whitelisting/blacklisting and blocked email checks. The account creation flow now tracks rejected emails client-side for real-time validation feedback, while error responses include structured data for improved UX.

Changes

Cohort / File(s) Summary
Email Validation Infrastructure
app/models/blocked_email.rb, lib/validators/email_validator.rb, app/models/user.rb
Introduced BlockedEmail model with tracking logic (should_block?, match_count, last_match_at); created EmailValidator class for domain restrictions and blocked email enforcement; removed custom email validation methods from User model in favor of declarative validators.
Account Creation Flow
app/assets/javascripts/discourse/controllers/create_account_controller.js, app/controllers/users_controller.rb
Added rejectedEmails array to track failed attempts client-side for continuous validation; enhanced error response with structured errors hash and values subset for client consumption.
Database & Configuration
db/migrate/20130724201552_create_blocked_emails.rb, config/locales/server.en.yml
Created blocked_emails table with email uniqueness index, action type, and match tracking columns; added email.blocked translation for error messaging.
Tests & Fixtures
spec/models/blocked_email_spec.rb, spec/fabricators/blocked_email_fabricator.rb, spec/components/validators/email_validator_spec.rb
Added test suite for BlockedEmail model validation and should_block? behavior; created fabricator for test data generation; added tests for EmailValidator blocking logic.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Browser)
    participant Controller as CreateAccountController
    participant UsersController as UsersController
    participant UserModel as User Model
    participant EmailValidator as EmailValidator
    participant BlockedEmail as BlockedEmail

    User->>Controller: Submit account form (email)
    Controller->>Controller: Check accountEmail against rejectedEmails
    alt Email in rejectedEmails
        Controller-->>User: Validation error (skip server)
    else Email not rejected
        Controller->>UsersController: POST /users/create
        UsersController->>UserModel: Create user
        UserModel->>EmailValidator: Validate email attribute
        EmailValidator->>EmailValidator: Check domain whitelist/blacklist
        EmailValidator->>BlockedEmail: should_block?(email)
        BlockedEmail->>BlockedEmail: Lookup email record, increment match_count
        alt Email is blocked
            BlockedEmail-->>EmailValidator: true
            EmailValidator-->>UserModel: Add validation error
        else Email allowed
            BlockedEmail-->>EmailValidator: false
            EmailValidator-->>UserModel: Pass validation
        end
        alt Validation fails
            UserModel-->>UsersController: Validation errors
            UsersController-->>User: JSON response (errors, values)
            User->>Controller: Receive failed email in response
            Controller->>Controller: Add email to rejectedEmails
        else Validation passes
            UserModel-->>UsersController: User created
            UsersController-->>User: Success response
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The changes span multiple cohorts with mixed complexity: the new BlockedEmail model and EmailValidator class introduce straightforward logic, but their integration with existing User validation requires verifying the removal of custom methods doesn't break undocumented behavior. The client-side controller changes are simple, but the interaction between client tracking and server responses warrants careful verification of the error flow.

Poem

🐰 A validation tale so fine,
Where blocked emails draw a line,
With models new and validators true,
Account creation shines anew,
Rejected lists in memory stay,
To guide users the better way! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: blocked-email-validation-post' is vague and generic, using a test automation descriptor rather than describing the actual technical changes made to the codebase. Revise the title to clearly describe the main change, such as 'Add email blocking validation system' or 'Implement BlockedEmail model for email validation', to better reflect the substantial feature addition across multiple files.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch blocked-email-validation-post

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/assets/javascripts/discourse/controllers/create_account_controller.js`:
- Around line 274-276: The code pushes result.values.email into rejectedEmails
without ensuring it exists; modify the block in create_account_controller (the
if handling result.errors && result.errors.email) to first check that
result.values.email is defined/non-falsy before calling
createAccountController.get('rejectedEmails').pushObject(...). Use a guard like
checking result.values && result.values.email (or optional chaining) and only
push when a valid email string is present to avoid inserting undefined into
rejectedEmails.

In `@app/models/blocked_email.rb`:
- Around line 11-19: The lookup in BlockedEmail.should_block? uses
BlockedEmail.where(email: email) which is case-sensitive; change the lookup to
use a downcased email (e.g., normalize the incoming email with
email.to_s.downcase before querying) and update the record logic in
should_block? to use the normalized value; also add a before_validation callback
(e.g., before_validation :normalize_email with a normalize_email method) or
ensure set_defaults downcases self.email on create/update so stored emails are
always lowercased.

In `@lib/validators/email_validator.rb`:
- Around line 18-22: The regex in email_in_restriction_setting?(setting, value)
currently uses Regexp.new("@(#{domains})", true) which can match domains in the
middle of the string; update the pattern to anchor to the absolute end (use \z)
so it only matches the domain at the end of the email (e.g.
Regexp.new("@(#{domains})\\z", true)), keeping the existing domains escaping
logic and case-insensitive flag.
🧹 Nitpick comments (7)
config/locales/server.en.yml (1)

733-734: Consider adding guidance to the blocked message.

The not_allowed message suggests "Please use another email address," but the new blocked message simply says "is not allowed." For UX consistency, consider adding similar guidance so users know what to do next.

app/assets/javascripts/discourse/controllers/create_account_controller.js (1)

17-17: Shared mutable default on the class prototype.

In classic Ember, object/array defaults on extend() are shared across all instances. Since Discourse controllers are singletons this is safe in practice, but the idiomatic pattern is to initialize mutable properties in init to prevent subtle bugs if the controller is ever instantiated more than once.

app/models/blocked_email.rb (1)

11-19: should_block? has a side effect (updating statistics) despite its predicate name.

This method both queries and mutates state. If this is intentional for analytics, consider documenting it or extracting the tracking into a separate method to avoid surprising callers.

lib/validators/email_validator.rb (1)

13-13: Prefer && over and for boolean logic.

Ruby's and has lower precedence than assignment and can cause subtle bugs in more complex expressions. && is the idiomatic choice for conditionals.

Proposed fix
-   if record.errors[attribute].blank? and BlockedEmail.should_block?(value)
+   if record.errors[attribute].blank? && BlockedEmail.should_block?(value)
spec/components/validators/email_validator_spec.rb (3)

3-7: Minor formatting: extra blank lines at block boundaries.

RuboCop flags extra empty lines at the describe block body beginning (after Line 3) and end (before Line 23).

♻️ Suggested fix
 describe EmailValidator do
-
   let(:record) { Fabricate.build(:user, email: "bad@spamclub.com") }

and at the end:

     end
   end
-
 end

13-13: Old RSpec should syntax used.

This uses the deprecated should/should_not expectation syntax. However, this project predominantly uses the old syntax throughout the test suite (2467 instances of should vs 294 of expect), so refactoring this file in isolation may create inconsistency. Consider this a low-priority modernization if the entire test suite is eventually migrated to modern RSpec conventions.


9-21: Add test coverage for domain whitelist/blacklist validation.

The tests cover the BlockedEmail.should_block? path. However, the EmailValidator implementation also validates email_domains_whitelist and email_domains_blacklist site settings (via email_in_restriction_setting?), but these paths have no test coverage. Add contexts for both settings to ensure the validator correctly enforces domain restrictions.

Comment on lines +274 to +276
if (result.errors && result.errors.email && result.values) {
createAccountController.get('rejectedEmails').pushObject(result.values.email);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against result.values.email being undefined.

If result.values is present but result.values.email is falsy/undefined, pushObject(undefined) would pollute the rejectedEmails array.

Proposed fix
-        if (result.errors && result.errors.email && result.values) {
-          createAccountController.get('rejectedEmails').pushObject(result.values.email);
+        if (result.errors && result.errors.email && result.values && result.values.email) {
+          createAccountController.get('rejectedEmails').pushObject(result.values.email);
         }
🤖 Prompt for AI Agents
In `@app/assets/javascripts/discourse/controllers/create_account_controller.js`
around lines 274 - 276, The code pushes result.values.email into rejectedEmails
without ensuring it exists; modify the block in create_account_controller (the
if handling result.errors && result.errors.email) to first check that
result.values.email is defined/non-falsy before calling
createAccountController.get('rejectedEmails').pushObject(...). Use a guard like
checking result.values && result.values.email (or optional chaining) and only
push when a valid email string is present to avoid inserting undefined into
rejectedEmails.

Comment on lines +11 to +19
def self.should_block?(email)
record = BlockedEmail.where(email: email).first
if record
record.match_count += 1
record.last_match_at = Time.zone.now
record.save
end
record && record.action_type == actions[:block]
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Case-sensitive email lookup can be bypassed with mixed-case input.

where(email: email) performs a case-sensitive match on most PostgreSQL configurations. A user could bypass blocking by registering with a differently-cased variant (e.g., Block@SpamFromHome.org vs. the stored block@spamfromhome.org). Downcase the email before lookup.

Proposed fix
  def self.should_block?(email)
-   record = BlockedEmail.where(email: email).first
+   record = BlockedEmail.where(email: email.downcase).first
    if record
      record.match_count += 1
      record.last_match_at = Time.zone.now
      record.save
    end
    record && record.action_type == actions[:block]
  end

Also ensure emails are downcased on creation (in set_defaults or a separate callback):

before_validation :normalize_email

def normalize_email
  self.email = email.downcase if email.present?
end
🤖 Prompt for AI Agents
In `@app/models/blocked_email.rb` around lines 11 - 19, The lookup in
BlockedEmail.should_block? uses BlockedEmail.where(email: email) which is
case-sensitive; change the lookup to use a downcased email (e.g., normalize the
incoming email with email.to_s.downcase before querying) and update the record
logic in should_block? to use the normalized value; also add a before_validation
callback (e.g., before_validation :normalize_email with a normalize_email
method) or ensure set_defaults downcases self.email on create/update so stored
emails are always lowercased.

Comment on lines +18 to +22
def email_in_restriction_setting?(setting, value)
domains = setting.gsub('.', '\.')
regexp = Regexp.new("@(#{domains})", true)
value =~ regexp
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Regex lacks end-of-string anchor — domain matching can produce false positives/negatives.

The pattern @(#{domains}) matches the domain anywhere after @, so user@blacklisted.com.legit.com would match blacklisted.com, and conversely user@whitelisted.com.evil.com would pass the whitelist. Anchor the regex to the end of the string.

Proposed fix
  def email_in_restriction_setting?(setting, value)
    domains = setting.gsub('.', '\.')
-   regexp = Regexp.new("@(#{domains})", true)
+   regexp = Regexp.new("@(#{domains})\\z", true)
    value =~ regexp
  end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def email_in_restriction_setting?(setting, value)
domains = setting.gsub('.', '\.')
regexp = Regexp.new("@(#{domains})", true)
value =~ regexp
end
def email_in_restriction_setting?(setting, value)
domains = setting.gsub('.', '\.')
regexp = Regexp.new("@(#{domains})\\z", true)
value =~ regexp
end
🤖 Prompt for AI Agents
In `@lib/validators/email_validator.rb` around lines 18 - 22, The regex in
email_in_restriction_setting?(setting, value) currently uses
Regexp.new("@(#{domains})", true) which can match domains in the middle of the
string; update the pattern to anchor to the absolute end (use \z) so it only
matches the domain at the end of the email (e.g. Regexp.new("@(#{domains})\\z",
true)), keeping the existing domains escaping logic and case-insensitive flag.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant